#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Net.h>
#include <Kharlia/Engine/Units.h>
#include <Kharlia/Engine/World.h>
#include <Kharlia/Engine/Level.h>
#include <Kharlia/Engine/Actor.h>

namespace kharlia { namespace engine {

FFieldInfo const* GetSentryField() {
    static FFieldInfo sentry = {FT_SENTRY};
    return &sentry;
}

int GetFieldSize(EFieldType type) {
    // TODO: Should be some magic template way to do this much more nicely
    switch (type) {
    case FT_SENTRY:
        throw RuntimeError();
    case FT_INT:
        return sizeof(int);
    case FT_UINT:
        return sizeof(int);
    case FT_FLOAT:
        return sizeof(float);
    case FT_DOUBLE:
        return sizeof(double);
    case FT_ENUM:
        return sizeof(int);
    case FT_BOOL:
        return sizeof(bool);
    case FT_PYOBJECT:
        return sizeof(object);
    case FT_PYSTR:
        return sizeof(str);
    case FT_PYUNICODE:
        return sizeof(object);
    case FT_VECTOR3F:
        return sizeof(FVector3F);
    case FT_ACTOR:
        return sizeof(shared_ptr<void>);
    default:
        throw RuntimeError();
    }
}

FFieldInfo const* GetFieldInfo(FFieldInfo const** fields, char const* name) {
    FFieldInfo const* info = nullptr;
    for (int i = 0; fields[i]->Type != FT_SENTRY; i++) {
        if (strcmp(fields[i]->Name, name) == 0) {
            info = fields[i];
            break;
        }
    }
    return info;
}

rn::Connection_RM3* FReplicaManager::AllocConnection(rn::SystemAddress const& addr, rn::RakNetGUID guid) const {
    auto conn = new FRMConnection(addr, guid);
    conn->mReplicaManager = const_cast<FReplicaManager*>(this);
    return conn;
}

void FReplicaManager::DeallocConnection(rn::Connection_RM3* connb) const {
    FRMConnection* conn = dynamic_cast<FRMConnection*>(connb);
    assert(conn);
    conn->mReplicaManager = nullptr;
    delete conn;
}

rn::Replica3* FReplicaManager::GetReplicaByNetworkID(rn::NetworkID networkId) {
    return rn::ReplicaManager3::GetReplicaByNetworkID(networkId);
}

void FReplicaManager::ReferenceExisting() {
    assert(mWorld);
    std::vector<CLevel*> const& levelptrs = mWorld->GetLevelPtrs();
    for (uint i = 0; i < levelptrs.size(); i++) {
        std::vector<TPtr<CActor>> const& actors = levelptrs[i]->GetActors();
        for (uint j = 0; j < actors.size(); j++) {
            CActor* a = actors[j].GetPtr();
            // If it already exists, it shouldn't be referenced
            if (a->GetNetwork())
                Reference(a->GetNetwork().GetPtr());
            // If it needs to be created, it will reference itself
            a->UpdateNetworkComponent();
            assert(!a->GetNetwork() || a->GetNetwork()->GetReplicaManager() == this);
        }
    }
}

FNetPeer::FNetPeer():
    mBase(nullptr),
    mIsEnabled(false),
    mIsServer(false),
    mIsClient(false),
    mReplicaManager(nullptr),
    mHandler(nullptr)
{
}

FNetPeer::~FNetPeer() {}

static_assert(sizeof(rn::MessageID) == sizeof(uint8), "");

void FNetPeer::Tick(float delta) {
    if (!mIsEnabled)
        return;
    assert(mBase && mHandler);

    rn::Packet* p;
    uint8 pid;
    bool dealloc, lost;
    while ((p = mBase->Receive()) != nullptr) {
        pid = GetPacketIdentifier(p);
        dealloc = true;

        switch (pid) {
        // RakNet messages
        case ID_CONNECTION_REQUEST_ACCEPTED:
            if (mConnectingTo != rn::SystemAddress()) {
                char addr[32];
                p->systemAddress.ToString(true, addr);
                GetLoggerPtr()->Info(format("Connection request accepted: %1%") % addr);

                mIsClient = true;
                mConnectingTo = rn::SystemAddress();
                mServerConnection = p->guid;

                mHandler->OnServerConnected(p->guid, p->systemAddress);
            }
            else {
                assert(mConnectingTo != rn::SystemAddress());
                GetLoggerPtr()->Info("ignoring connection request accepted while not connecting");
            }
            break;

        case ID_NEW_INCOMING_CONNECTION:
            // We're the server and a new client connected
            if (mIsServer) {
                char addr[32];
                p->systemAddress.ToString(true, addr);
                GetLoggerPtr()->Info(format("New incomming connection: %1%") % addr);

                mClientConnections.push_back(p->guid);

                mHandler->OnClientConnected(p->guid, p->systemAddress);
            }
            else {
                GetLoggerPtr()->Info("New connection dismissed");
            }
            break;

        case ID_DISCONNECTION_NOTIFICATION:
        case ID_CONNECTION_LOST:
            lost = (pid == ID_CONNECTION_LOST);
            if (mIsClient) {
                GetLoggerPtr()->Info("Server has disconnected");

                mHandler->OnServerDisconnected(p->guid, p->systemAddress, lost);

                mIsClient = false;
                mServerConnection = rn::RakNetGUID();
            }
            else if (mIsServer) {
                char addr[32];
                p->systemAddress.ToString(true, addr);
                GetLoggerPtr()->Info(str("Client disconnected: ") + str(addr));

                mHandler->OnClientDisconnected(p->guid, p->systemAddress, lost);

                auto it = std::find(mClientConnections.begin(), mClientConnections.end(), p->guid);
                assert(it != mClientConnections.end());
                mClientConnections.erase(it);
            }
            else {
                GetLoggerPtr()->Info("Ignored disconnection/connection lost message");
            }
            break;

        case ID_CONNECTION_BANNED:
            {
                assert(mConnectingTo != rn::SystemAddress());
                GetLoggerPtr()->Info("Failed to connect to server due to ban");
                mConnectingTo = rn::SystemAddress();
            }
            break;

        // Engine Messages
        //case ID_DEBUG_MESSAGE:
        //    {
        //        char msg[DEBUG_MESSAGE_MAX_SIZE];
        //        rn::BitStream input(p->data, p->length, false);
        //        input.IgnoreBytes(sizeof(rn::MessageID));
        //        rn::StringCompressor::Instance()->DecodeString(
        //            msg, DEBUG_MESSAGE_MAX_SIZE, &input);
        //        GetLoggerPtr()->Debug(str("Net debug message: ") + str(msg));
        //    }
        //    break;

        //case ID_ACTOR_RPC:
        //    {
        //        GetLoggerPtr()->Debug("received actor RPC packet");
        //        rn::BitStream input(p->data, p->length, false);
        //        IgnorePacketIdentifier(&input);
        //        NetOnReceiveActorRPC(&input);
        //    }
        //    break;

        //case ID_CLIENT_SETUP:
        //    // Sent from server to client after client successfully connects to server
        //    // and has a player controller created
        //    {
        //        if (mIsServer)
        //            break;
        //        rn::BitStream input(p->data, p->length, false);
        //        IgnorePacketIdentifier(&input);
        //        rn::NetworkID nid;
        //        input.Read(nid);
        //        assert(nid != rn::UNASSIGNED_NETWORK_ID);
        //        assert(mDesiredPCID == rn::UNASSIGNED_NETWORK_ID);
        //        mDesiredPCID = nid;
        //    }
        //    break;

        default:
            dealloc = mHandler->OnReceive(*p, pid);
            break;
        }

        if (dealloc)
            mBase->DeallocatePacket(p);
    }
}

void FNetPeer::Listen() {
    if (mIsServer)
        throw RuntimeError("already listening");
    
    assert(!mBase && mHandler);
    mBase = rn::RakPeerInterface::GetInstance();
    rn::StartupResult result = mBase->Startup(5, &rn::SocketDescriptor(60500, nullptr), 1);

    if (result != rn::RAKNET_STARTED)
        throw RuntimeError("RakNet failed to startup");

    mBase->SetOccasionalPing(true);
#ifdef _DEBUG
    mBase->SetTimeoutTime(300000, rn::UNASSIGNED_SYSTEM_ADDRESS);
#endif
    mBase->SetMaximumIncomingConnections(4);

    mIsEnabled = true;
    mIsServer = true;
}

void FNetPeer::Connect(str host, uint16 port) {
    assert(mHandler);
    if (!mBase) {
        mBase = rn::RakPeerInterface::GetInstance();
        rn::StartupResult result = mBase->Startup(1, &rn::SocketDescriptor(0, nullptr), 1);

        if (result != rn::RAKNET_STARTED)
            throw RuntimeError("RakNet failed to startup");
        mBase->SetOccasionalPing(true);
#ifdef _DEBUG
        mBase->SetTimeoutTime(300000, rn::UNASSIGNED_SYSTEM_ADDRESS);
#endif
        mIsEnabled = true;
    }
    if (mConnectingTo != rn::UNASSIGNED_SYSTEM_ADDRESS) {
        assert(mBase->GetConnectionState(mConnectingTo) == rn::IS_CONNECTING);
        mBase->CancelConnectionAttempt(mConnectingTo);
        mConnectingTo = rn::UNASSIGNED_SYSTEM_ADDRESS;
    }
    
    ShutdownReplicaManager();

    char const* chost = extract<char const*>(host);
    rn::ConnectionAttemptResult cresult = mBase->Connect(chost, port, nullptr, 0);
    if (cresult != rn::CONNECTION_ATTEMPT_STARTED)
        throw RuntimeError("connection attempt failed to start");

    mConnectingTo = rn::SystemAddress(chost, port);
}

void FNetPeer::Shutdown() {
    assert(mIsEnabled == (bool)mBase);
    if (!mIsEnabled)
        return;

    ShutdownReplicaManager();

    mServerConnection = rn::RakNetGUID();
    mClientConnections.clear();

    mBase->Shutdown(2000);
    rn::RakPeerInterface::DestroyInstance(mBase);
    mBase = nullptr;

    mIsEnabled = false;
    mIsClient = false;
    mIsServer = false;
    mConnectingTo = rn::UNASSIGNED_SYSTEM_ADDRESS;
}

bool FNetPeer::CloseConnection(rn::AddressOrGUID target, bool notify) {
    assert(mBase && !target.IsUndefined());

    if (target.rakNetGuid == rn::UNASSIGNED_RAKNET_GUID) {
        assert(target.systemAddress != rn::UNASSIGNED_SYSTEM_ADDRESS);
        target.rakNetGuid = mBase->GetGuidFromSystemAddress(target.systemAddress);
        if (target.rakNetGuid == rn::UNASSIGNED_RAKNET_GUID)
            return false;
    }

    mBase->CloseConnection(target, notify);

    bool removed = false;
    if (mServerConnection == target.rakNetGuid) {
        assert(mIsClient);
        mServerConnection = rn::UNASSIGNED_RAKNET_GUID;
        removed = true;
    }

    for (uint i = 0; i < mClientConnections.size(); i++) {
        if (mClientConnections[i] == target.rakNetGuid) {
            assert(mIsServer);
            mClientConnections.erase(mClientConnections.begin() + i);
            removed = true;
            break;
        }
    }

    assert(removed);
    return true;
}

void FNetPeer::InitReplicaManager(CWorld* world) {
    assert(mBase && world);
    if (mReplicaManager)
        ShutdownReplicaManager();
    mReplicaManager = new FReplicaManager(this, world);
    mReplicaManager->SetAutoSerializeInterval(30);
    mReplicaManager->SetDefaultOrderingChannel(5);
    mReplicaManager->SetDefaultPacketPriority(MEDIUM_PRIORITY);
    mReplicaManager->SetDefaultPacketReliability(RELIABLE_SEQUENCED);
    mReplicaManager->SetNetworkIDManager(&mIDManager);
    mReplicaManager->SetAutoManageConnections(false, false);
    mBase->AttachPlugin(mReplicaManager);
    mReplicaManager->ReferenceExisting();
}

void FNetPeer::ShutdownReplicaManager() {
    if (!mReplicaManager)
        return;

    mBase->DetachPlugin(mReplicaManager);
    delete mReplicaManager;
    mReplicaManager = nullptr;
}

}}